package org.dq.base.page.impl;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.List;
import java.util.Properties;


import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.apache.ibatis.session.ResultHandler;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.dq.base.page.Page;
import org.dq.base.page.PageEngine;
import org.dq.base.page.PageParam;


/**
 * 本拦截器将拦截StatementHandler(Statement/PreparedStatement/CallabledStatement对象处理器)的如下方法：
 * 	prepare(Connection) 负责sql语句的准备工作
 * 	query(Statement,ResultHandler) 负责执行查询操作
 * 
 * @author Administrator
 *
 */
@Intercepts(
		{
			@Signature(type=StatementHandler.class,method="prepare",args={Connection.class,Integer.class}),
			@Signature(type=StatementHandler.class,method="query",args={Statement.class,ResultHandler.class})		
		}
)
public class MySqlPageInterceptor implements Interceptor {
	
	private static Logger LOG=LogManager.getLogger(MySqlPageInterceptor.class);
	
	
	public MySqlPageInterceptor() {
		super();
		LOG.info("MyBatis插件"+this.getClass().getName()+"加载！");
	}


	
	//Dao对象方法匹配样式（正则表达式）
	private String methodPattern=
			"(^com\\.tnr\\.dao\\..*Mapper\\..+Find.*Page.*$)|(^com\\.tnr\\.dao\\..*Mapper\\.find.*Page.*$)";
	
	
	/**
	 * 拦截方法
	 * Invocation invocation 表示被拦截的（正在执行的）方法对象
	 */
	@Override
	public Object intercept(Invocation invocation) throws Throwable {
		
		
		if(invocation.getMethod().getName().equals("prepare")){//如果被拦截的方法名为“parpare”
			
			return this.interceptStatementHandlerPrepare(invocation);
			
		}else{//如果被拦截的方法名为“query”
			
			return this.interceptStatementHandlerQuery(invocation);
		}
		
	}

	
	/**
	 * MyBatis将调用该方法，将被拦截对象通过本拦截器包装并代理。
	 */
	@Override
	public Object plugin(Object target) {
		return Plugin.wrap(target, this);  
	}

	/**
	 * 为拦截器设置自定义属性
	 */
	@Override
	public void setProperties(Properties props) {
		this.methodPattern = props.getProperty("methodPattern");
		LOG.debug(" >>> [MyBatisMySql分页拦截器-属性设置]methodPattern="+this.methodPattern);		
	}

	//处理对StatementHandler对象的prepare方法的拦截
	private  Object interceptStatementHandlerPrepare(Invocation invocation) throws Throwable{
		
		//获取被拦截的StatementHandler对象
		StatementHandler statementHandler=(StatementHandler)invocation.getTarget();	
		
		//从StatementHandler对象获取绑定sql对象。
		BoundSql boundSql=statementHandler.getBoundSql();
		
		//从绑定sql对象中获取映射器中传递的参数对象
		Object parameter = boundSql.getParameterObject();
		
		//获取StatementHandler对象的元数据对象（注：该工具对象作用利用Java反射机制访问对象的私有属性和方法）
		MetaObject statementHandlerMeta =SystemMetaObject.forObject(statementHandler);
		
		//获取StatementHandler对象的私有属性delegate的mappedStatement属性，即获取封装了映射语句对象（该对象代表了映射注解或映射xml）
		MappedStatement mappedStatement=(MappedStatement) statementHandlerMeta.getValue("delegate.mappedStatement");
		
		//如果映射语句对象的id不匹配制定的方法样式则直接放行invocation对象，即不再拦截。
		LOG.debug("methodPattern="+methodPattern);
		if(!mappedStatement.getId().matches(methodPattern)){
			return invocation.proceed();
		}
		
		LOG.debug(" >>> [MyBatisMySql分页拦截器(拦截SQL)-映射器ID]"+mappedStatement.getId());
		
		/*if(!matchePattern(mappedStatement.getId(),parameter)){
			return invocation.proceed();
		}*/
		
		//从绑定sql对象中获取sql语句		
		String sql=boundSql.getSql();
		
		//将参数对象转化为ParaParam对象
		PageParam pageParam=(PageParam) parameter;
		
		//计算从记录数
		final int _total=getTotal((Connection) invocation.getArgs()[0], statementHandler, sql);
		
		LOG.debug(" >>> [MyBatisMySql分页拦截器(拦截SQL)-查询记录总数]"+_total);
		
		//创建分页引擎（此时分页引擎暂无法实现getData方法）
		PageEngine<Object> pageEngine=new PageEngine<Object>(){

			@Override
			public int getTotal() throws SQLException {
				return _total;
			}

			@Override
			public List<Object> getData(int start, int end) throws SQLException {
				return null;
			}
	
		};
		
		//创建分页对象（此时分页对象中尚无当前页数据记录）
		Page page=new PageImpl(pageEngine,pageParam.getRows(),pageParam.getPage());
		
		
		//将分页对象设置到分页参数对象中
		MetaObject pageParamMeta=SystemMetaObject.forObject(pageParam);
		pageParamMeta.setValue("pageObj", page);
		//pageParam.setPageObj(page);
		
		LOG.debug(" >>> [MyBatisMySql分页拦截器(拦截SQL)-创建Page对象]将Page对象放入参数对象中。");
		
		//拼接查询分页数据的sql
		String pageDataSql=sql+" limit "+(page.getCurr()-1)*page.getRows()+","+page.getRows();
		
		//将新的查询分页数据的sql语句重新设置到StatementHandler对象中
		statementHandlerMeta.setValue("delegate.boundSql.sql", pageDataSql);
		
		debugSQL(" >>> [MyBatisMySql分页拦截器(拦截SQL)-修改SQL]",pageDataSql);
		
		
		
		return invocation.proceed();
		
	}
	
	//处理对StatementHandler对象的query方法的拦截
	private Object interceptStatementHandlerQuery(Invocation invocation) throws Throwable{
		
		StatementHandler statementHandler=(StatementHandler)invocation.getTarget();	
		
		MetaObject statementHandlerMeta =SystemMetaObject.forObject(statementHandler);
		MappedStatement mappedStatement=(MappedStatement) statementHandlerMeta.getValue("delegate.mappedStatement");
		
		//如果映射语句对象的id不匹配制定的方法样式则直接放行invocation对象，即不再拦截。
		if(!mappedStatement.getId().matches(methodPattern)){
			return invocation.proceed();
		}
		LOG.debug(" >>> [MyBatisMySql分页拦截器(拦截查询)-映射器ID]"+mappedStatement.getId());
		
		//执行查询，获取查询结果
		Object list= invocation.proceed();
		
		//将查询结果放到分页对象中，此时分页对象中缺少的分页数据被补全了
		statementHandlerMeta.setValue("delegate.boundSql.parameterObject.pageObj.recList", list);
		
		LOG.debug(" >>> [MyBatisMySql分页拦截器(拦截查询)-截获查询]将查询结果成功放入Page对象中。");
		
		return list;
		 
		
	}
	

	
	/**
	 * 获取记录总数
	 * @param statementHandler 
	 * @return
	 * @throws SQLException 
	 */
	private int getTotal(Connection conn,StatementHandler statementHandler,String sql) throws SQLException{
		
		PreparedStatement pst=null;
		ResultSet rs=null;
		try {
			String cntsql="select count(9) "+sql.substring(sql.toUpperCase().indexOf("FROM"));
			
			debugSQL(" >>> [MyBatisMySql分页拦截器(拦截SQL)-生成查询记录总数SQL]", cntsql);
			
			pst = conn.prepareStatement(cntsql);
			statementHandler.parameterize(pst);
			rs = pst.executeQuery();
			int total=0;
			if(rs.next()){
				total=rs.getInt(1);
			}
			return total;
		} catch (SQLException e) {
			throw e;
		}finally{
			try {
				rs.close();
			} catch (Exception e) {
				e.printStackTrace();
			}
			try {
				pst.close();
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
	}
	
	
	//调试输出sql
	private void debugSQL(String prefix,String sql) {
		if(LOG.isDebugEnabled()){
			
			StringBuilder _sql=new StringBuilder(sql.replace("\n", " ").replace("\r", " ").replace("	", " "));
			
			int i=0;
			while(i<_sql.length()){
				if((_sql.charAt(i)==' ' && i==0)||(i>0 && _sql.charAt(i)==' ' && _sql.charAt(i-1)==' ')){
					_sql.deleteCharAt(i);
					i--;
				}
				i++;
			}
			
			LOG.debug(prefix+_sql);
		}
		
	}

	
	/*
	private boolean matcheMethodPattern(Object parameter){
		try {
			StackTraceElement[] emts=Thread.currentThread().getStackTrace();
			for(StackTraceElement emt:emts){
				if(emt.getMethodName().matches(methodPattern)){
					Class<?>[] infs=Class.forName(emt.getClassName()).getInterfaces();
					for(Class<?> inf:infs){
						if(inf.getName().matches(daoPattern)){
							if(parameter instanceof PageParam){
								return true;
							}
						}
					}
				}
			}
			return false;
		} catch (ClassNotFoundException e) {
			throw new RuntimeException(e);
		}
	}
	*/
	
	/*
	private boolean matchePattern(String id, Object parameter) {
		int fg=id.lastIndexOf(".");//类型名称与方法名称的"."分隔符位置
		String mdName=id.substring(fg+1);
		String typeName=id.substring(0, fg);
		return mdName.matches(methodPattern)&&typeName.matches(daoPattern);
	}
	*/


}
